#pragma once

#include <cmath>

#include "base.h"
#include "../updater/ma.h"
#include "../impl/s2.h"

namespace mdtx::online::indicator
{
    /**
     * @brief Rolling sum
     *
     * @tparam T scala type
     */
    template <typename T>
    using rosum = detail::indi_wrap_ro<updater::sum<T>>;


    /**
     * @brief Cumulative sum
     *
     * @tparam T scala type
     */
    template <typename T>
    using cusum = detail::indi_wrap_cu<updater::sum<T>>;

    /**
     * @brief Simple moving average
     *
     * @tparam T scala type
     */
    template <typename T>
    using sma = detail::indi_wrap_ro<updater::sma<T>>;

    template <typename T>
    using rollmean = sma<T>;

    /**
     * @brief Cumulative moving average
     *
     * @tparam T scala type
     */
    template <typename T>
    using cma = detail::indi_wrap_cu<updater::sma<T>>;

    template <typename T>
    using cummean = cma<T>;

    /**
     * @brief Triangular moving average
     *
     * @tparam T scala type
     */
    template <typename T>
    class tma : public indi_unary1<T>
    {
        using base = indi_unary1<T>;

    public:
        using typename base::tuple_in;
        using typename base::tuple_out;
        using base::update;

    protected:
        boost::circular_buffer<T> buf;
        impl::sma<T> m1{}, m2{};

    public:
        explicit tma(ptrdiff_t period)
            : buf(period + (period % 2))
        {
        }

        tuple_out update(tuple_in const &in) override
        {
            T x = std::get<0>(in);
            if (buf.full())
            {
                m1.roll_impl(x, buf[0]);
                m2.roll_impl(m1.m, buf[1]);
            }
            else
            {
                m1.insert_impl(x);
                m2.insert_impl(m1.m);
            }
            bpush(buf, x, m1.m);

            return tuple_out(m2.m);
        }
    };

    /**
     * @brief Weighted (linear) moving average
     *
     * @tparam T scala type
     */
    template <typename T>
    using wma = detail::indi_wrap_ro<updater::wma<T>>;

    /**
     * @brief Linear (weighted) moving average
     *
     * @tparam T scala type
     */
    template <typename T>
    using lma = wma<T>;

    /**
     * @brief Hull moving average
     *
     * @tparam T scala type
     */
    template <typename T>
    class hma : public indi_unary1<T>
    {
        using base = indi_unary1<T>;

    public:
        using typename base::tuple_in;
        using typename base::tuple_out;
        using base::update;

    protected:
        wma<T> wma_m, wma_n, wma_s;

    public:
        explicit hma(ptrdiff_t period)
            : wma_m(period / 2),
              wma_n(static_cast<ptrdiff_t>(std::sqrt(period))),
              wma_s(period)
        {
        }

        tuple_out update(tuple_in const &in) override
        {
            auto mm = wma_m.update(in);
            auto mn = wma_n.update(mm);
            T dd = std::get<0>(mm) * 2 - std::get<0>(mn);
            return wma_s.update(tuple_in(dd));
        }
    };

    /**
     * @brief Exponential moving average
     *
     * @tparam T scala type
     */
    template <typename T>
    class ema : public detail::indi_wrap_cu<updater::ema<T>>
    {
        using wrapper = detail::indi_wrap_cu<updater::ema<T>>;

    public:
        explicit ema(ptrdiff_t period)
            : wrapper(period)
        {
        }
    };

    /**
     * @brief Double EMA
     *
     * @tparam T scala type
     */
    template <typename T>
    class dema : public indi_unary1<T>
    {
        using base = indi_unary1<T>;

    public:
        using typename base::tuple_in;
        using typename base::tuple_out;
        using base::update;

    protected:
        impl::xema<T, 2> ma;

    public:
        explicit dema(ptrdiff_t period)
            : ma(period)
        {
        }

        tuple_out update(tuple_in const &in) override
        {
            ma.insert_impl(std::get<0>(in));
            return tuple_out(ma.m[0] * 2 - ma.m[1]);
        }
    };

    /**
     * @brief Triple EMA
     *
     * @tparam T scala type
     */
    template <typename T>
    class tema : public indi_unary1<T>
    {
        using base = indi_unary1<T>;

    public:
        using typename base::tuple_in;
        using typename base::tuple_out;
        using base::update;

    protected:
        impl::xema<T, 3> ma;

    public:
        explicit tema(ptrdiff_t period)
            : ma(period)
        {
        }

        tuple_out update(tuple_in const &in) override
        {
            ma.insert_impl(std::get<0>(in));
            return tuple_out(ma.m[0] * 3 - ma.m[1] * 3 + ma.m[2]);
        }
    };

    /**
     * @brief T3 EMA
     *
     * @tparam T scala type
     */
    template <typename T>
    class t3ma : public indi_unary1<T>
    {
        using base = indi_unary1<T>;

    public:
        using typename base::tuple_in;
        using typename base::tuple_out;
        using base::update;

    protected:
        impl::xema<T, 6> ma;
        T c1, c2, c3, c4;

    public:
        explicit t3ma(ptrdiff_t period, T a = 0.7)
            : ma(period),
              c1(-std::pow(a, 3)),
              c2(3 * std::pow(a, 3) + 3 * std::pow(a, 2)),
              c3(-3 * std::pow(a, 3) - 6 * std::pow(a, 2) - 3 * a),
              c4(std::pow(a, 3) + 3 * std::pow(a, 2) + 3 * a + 1)
        {
        }

        tuple_out update(tuple_in const &in) override
        {
            ma.insert_impl(std::get<0>(in));
            T val = c1 * ma.m[5] +
                    c2 * ma.m[4] +
                    c3 * ma.m[3] +
                    c4 * ma.m[2];
            return tuple_out(val);
        }
    };

    /**
     * @brief Exponential window moving average
     *
     * @tparam T scala type
     */
    template <typename T>
    class ewma : public detail::indi_wrap_ro<updater::ewma<T>>
    {
        using wrapper = detail::indi_wrap_ro<updater::ewma<T>>;

    public:
        explicit ewma(ptrdiff_t period)
            : wrapper(period, period)
        {
        }
    };

    /**
     * @brief Zero-lag EMA
     *
     * @tparam T scala type
     */
    template <typename T>
    class zlema : public indi_unary1<T>
    {
        using base = indi_unary1<T>;

    public:
        using typename base::tuple_in;
        using typename base::tuple_out;
        using base::update;

    protected:
        impl::ema<T> ma;
        boost::circular_buffer<T> buf;

    public:
        explicit zlema(ptrdiff_t period)
            : ma(T(2) / (period + 1)),
              buf(period / 2)
        {
        }

        tuple_out update(tuple_in const &in) override
        {
            T x = std::get<0>(in), datum;
            if (buf.full())
            {
                datum = x + (x - buf.front());
            }
            else
            {
                datum = x;
            }
            buf.push_back(x);
            ma.insert_impl(datum);

            return tuple_out(ma.m);
        }
    };

    /**
     * @brief Wilders moving average
     *
     * @tparam T scala type
     */
    template <typename T>
    class wilders : public detail::indi_wrap_cu<updater::ema<T>>
    {
        using base = detail::indi_wrap_cu<updater::ema<T>>;

    public:
        explicit wilders(ptrdiff_t period)
            : base(T(1) / period)
        {
        }
    };

    /**
     * @brief Volume/value weighted cumulative moving average
     *
     * @tparam T scala type
     */
    template <typename T>
    using cma_vw = detail::indi_wrap_cu<updater::wsma<T>>;

    /**
     * @brief Volume/value weighted simple moving average
     *
     * @tparam T scala type
     */
    template <typename T>
    using sma_vw = detail::indi_wrap_ro<updater::wsma<T>>;

    /**
     * @brief Elastic volume weighted moving average
     *
     * @tparam T scala type
     */
    template <typename T>
    class evwma : public indi_weighted1<T>
    {
        using base = indi_weighted1<T>;

    public:
        using typename base::tuple_in;
        using typename base::tuple_out;
        using base::update;

    protected:
        boost::circular_buffer<T> buf;
        impl::sum<T> wsum{};
        T m{};
        kahan<T> acc_m{};

    public:
        explicit evwma(ptrdiff_t period)
            : buf(period)
        {
        }

        tuple_out update(tuple_in const &in) override
        {
            auto [p, v] = in;
            if (buf.full())
            {
                wsum.roll_impl(v, buf.front());
            }
            else
            {
                wsum.insert_impl(v);
            }
            buf.push_back(v);

            auto w = v / wsum.s;
            acc_m(m, w * (p - m));

            return tuple_out(m);
        }
    };

    /**
     * @brief Variable index dynamic average
     *
     * @tparam T scala type
     */
    template <typename T>
    class vidya : public indi_unary1<T>
    {
        using base = indi_unary1<T>;

    public:
        using typename base::tuple_in;
        using typename base::tuple_out;
        using base::update;

    protected:
        boost::circular_buffer<T> buf;
        impl::sum<T> sup{}, sdown{};
        T a, m{}, last{};
        kahan<T> acc_m{};
        bool init{};

    public:
        explicit vidya(ptrdiff_t period)
            : buf(period * 2),
              a(T(2) / (period + 1))
        {
        }

        tuple_out update(tuple_in const &in) override
        {
            T x = std::get<0>(in);

            if (init)
            {
                // CMO
                T up{}, down{};
                if (x > last)
                {
                    up = x - last;
                }
                else if (x < last)
                {
                    down = last - x;
                }
                if (buf.full())
                {
                    sup.roll_impl(up, buf[0]);
                    sdown.roll_impl(down, buf[1]);
                }
                else
                {
                    sup.insert_impl(up);
                    sdown.insert_impl(down);
                }
                bpush(buf, up, down);

                // smooth
                T r{};
                if (sup.s != sdown.s)
                {
                    r = std::abs(sup.s - sdown.s) / (sup.s + sdown.s);
                }
                acc_m(m, a * r * (x - m));
            }
            else
            {
                m = x;
                init = true;
            }
            last = x;

            return tuple_out(m);
        }
    };

    /**
     * @brief Kaufman adaptive moving average
     *
     * @tparam T scala type
     */
    template <typename T>
    class kama : public indi_unary1<T>
    {
        using base = indi_unary1<T>;

    public:
        using typename base::tuple_in;
        using typename base::tuple_out;
        using base::update;

    protected:
        constexpr static T f = T(2) / 3;
        constexpr static T s = T(2) / 31;
        constexpr static T fs = f - s;
        boost::circular_buffer<T> buf;
        impl::sum<T> d1sum{};
        T m{};
        kahan<T> acc_m{};
        bool init{};

    public:
        explicit kama(ptrdiff_t period = 26)
            : buf(period * 2)
        {
        }

        tuple_out update(tuple_in const &in) override
        {
            T x = std::get<0>(in), d1{};
            if (init)
            {
                // buf order: (d1, x)...
                d1 = std::abs(x - buf.back());
                T dn = std::abs(x - buf[1]);
                if (buf.full())
                {
                    d1sum.roll_impl(d1, buf.front());
                    T a = std::pow(dn / d1sum.s * fs + s, 2);
                    acc_m(m, a * (x - m));
                }
                else
                {
                    d1sum.insert_impl(d1);
                }
            }
            else
            {
                init = true;
                m = x;
            }
            bpush(buf, d1, x);

            return tuple_out(m);
        }
    };
}
